package br.ufes.cwtools.gw.android.connection_service;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.InputStreamBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;

import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;

public class ConnectionService extends Service {
	private static final String LOG_TAG = "ConnectionService";
	
	private static final String CACHE_NAME = "/Android/data/" + ConnectionService.class.getPackage().getName() + "/cache/";
	public static final String MAGIC_ERROR_STRING = "ERRO_NO_DOWNLOAD";
	
	public static enum REGISTER_STATUS {  // Códigos de retorno para o registro de requisições.
		OK (1), CACHE (2), ERROR (0);
		private final int code;
		REGISTER_STATUS(int code) { this.code = code; }
		public int getCode() { return code; }  // Retorna o código de status como int.
	}
	
	private RequestQueue mRequestQueue = new RequestQueue();
	private Cache mCache = new Cache();
	private File mCacheDir;
	private String loginUrl, username, password;
	
	@Override
	public void onCreate() {
		super.onCreate();
		Log.d(LOG_TAG, "Service created.");
		
		Log.v(LOG_TAG, "Cache location: " + CACHE_NAME);
		// Cria diretório para os arquivos temporários. (API level < 8)
		// /Android/data/<package_name>/cache/
		// Referência: http://developer.android.com/guide/topics/data/data-storage.html#filesExternal
		// TODO: Tratar erros quando o diretório não está acessível (sem SD card, por exemplo).
		mCacheDir = new File(Environment.getExternalStorageDirectory(), CACHE_NAME);
		mCacheDir.mkdirs();
		// TODO Limpar os arquivos temporários.
		File[] tempFiles = mCacheDir.listFiles();
		Log.v(LOG_TAG, "Delete " + tempFiles.length + " temporary files.");
		for (File f : tempFiles) {
			f.delete();
		}
		
		// Connectivity events receiver.
		registerReceiver(mReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
		
		// Create thread to process queue.
		mConsumerThread.start();
	}
	
	@Override
	public void onDestroy() {
		super.onDestroy();
		mConsumerThread.stopDownloads();
		unregisterReceiver(mReceiver);
	}
	
	private ConsumerThread mConsumerThread = new ConsumerThread();  // Processa as requisições.
	
	private BroadcastReceiver mReceiver = new BroadcastReceiver() {  // Recebe as notificações de conectividade.
		@Override
		public void onReceive(Context context, Intent intent) {
			NetworkInfo networkInfo = (NetworkInfo) intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO);
			if (networkInfo.isConnected()) {
				mConsumerThread.resumeDownloads();
			} else {
				mConsumerThread.pauseDownloads();
			}
		}
	};

	final private IConnectionService.Stub mBinder = new IConnectionService.Stub() {  // Interface AIDL de downlaod (GET).
		
		@Override
		public int registerGetRequest(String url, String requestor, String id,
				boolean allowCache, IConnectionServiceCallback callback)
				throws RemoteException {
			Log.d(LOG_TAG, "GET request. URL: " + url);
			
			final Request request = new Request(url, callback, requestor, id);
			
			// Try cache
			if (allowCache) {
				final String cached = mCache.get(url);
				if (cached != null) {
					Log.d(LOG_TAG, "Requested URL found in cache.");
					callback.requestFinished(request.getId(), cached);
					return REGISTER_STATUS.CACHE.getCode();
				}
			}
			
			if (mRequestQueue.addRequest(request)) return REGISTER_STATUS.OK.getCode();
			else return REGISTER_STATUS.ERROR.getCode();  // Retorna 0 indicando falha ao registrar o pedido.
		}
		
		@Override
		public int registerPostRequest(String url, String requestor, String id,
				Bundle stringParts, Bundle fileParts, IConnectionServiceCallback callback)
				throws RemoteException {
			Log.d(LOG_TAG, "POST request. URL: " + url);
			
			// Sem cache para POST.
			final Request request = new Request(url, callback, requestor, id, stringParts, fileParts);
			if (mRequestQueue.addRequest(request)) return REGISTER_STATUS.OK.getCode();
			else return REGISTER_STATUS.ERROR.getCode();  // Retorna 0 indicando falha ao registrar o pedido.
		}

		@Override
		public void unregister(String requestor, String id) throws RemoteException {
			mRequestQueue.removeRequest(requestor, id);
		}

		@Override
		public void unregisterAll(String requestor) throws RemoteException {
			Log.d(LOG_TAG, "Unregistering downloads from: " + requestor);
			mRequestQueue.removeRequests(requestor);
		}

		@Override
		public void setLoginData(String url, String username, String password)
				throws RemoteException {
			if (!url.startsWith("http://")){
				url = "http://" + url;
			}
			setLoginProcessData(url, username, password);
		}
	};
	
	private void setLoginProcessData(String url, String user, String pass){
		this.loginUrl = url;
		this.username = user;
		this.password = pass;
	}
	
	@Override
	public IBinder onBind(Intent intent) {
		if (intent.getAction().equals(IConnectionService.class.getName())) {
			return mBinder;
		}
		return null;
	}
	
	private String processRequest(Request request) throws IOException {
		try {
			DefaultHttpClient httpClient = new DefaultHttpClient();
			
			// TODO: verificar a necessidade de login a partir de um tempo de timeout configurável no ApplicationDefinition
			login(httpClient);
			
			final HttpResponse response = request.execute(httpClient, this);
			
			response.getEntity().getContentLength();
			InputStream is = response.getEntity().getContent();
		
			// Download contents and write to file.
			
			File file = File.createTempFile("temp_", null, mCacheDir);
			FileOutputStream os = new FileOutputStream(file);
			byte[] buffer = new byte[4096];
			int bytesRead;
			while ((bytesRead = is.read(buffer)) != -1) {
				os.write(buffer, 0, bytesRead);
			}
			is.close();
			os.close();
			
			return file.getPath();
		} catch (Exception e) {
			Log.v(LOG_TAG, "Exception", e);
			Log.v(LOG_TAG, request.getUrl() + " : " + request.getRequestor());
			
			//Creates file with error string
			
			File file = File.createTempFile("temp_", null, mCacheDir);
			FileOutputStream os = new FileOutputStream(file);
			
			String errorMessage = MAGIC_ERROR_STRING;
			errorMessage = errorMessage + " " + request.getRequestor() + " : " + request.getUrl();
			os.write(errorMessage.getBytes());
			os.close();
			
			Log.v(LOG_TAG, "Error handling!");
			Log.v(LOG_TAG, errorMessage);
			return file.getPath();
		}
	}
	
	private void login(DefaultHttpClient httpClient) {
		//Só faz o login depois que o serviço for configurado
		if (loginUrl == null || username == null || password == null){
			return;
		}
		
		Log.v(LOG_TAG, "LOGANDO em " + loginUrl + " com " + username + ":" + password);
		
		HttpClient hc = httpClient;
		HttpPost hp = new HttpPost(loginUrl);

		List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>();
		nameValuePairs.add(new BasicNameValuePair("user.login", username));
		nameValuePairs.add(new BasicNameValuePair("user.password", password));
		
		try {
			hp.setEntity(new UrlEncodedFormEntity(nameValuePairs));
			hc.execute(hp).getEntity().consumeContent(); //free connection resources
		} catch (Exception e) {
			e.printStackTrace();
		}
		Log.v(LOG_TAG, "Autenticação efetuada.");
	}

	private class ConsumerThread extends Thread {
		private volatile boolean mPause = false;
		private volatile boolean mStop = false;
		
		public void stopDownloads() {
			mStop = true;
			if (mPause) {
				synchronized (this) {
					notify();
				}
			}
			mRequestQueue.shake();  // Coloca um elemento para desbloquear a fila.
		}
		
		public void pauseDownloads() {
			mPause = true;
		}
		
		public void resumeDownloads() {
			mPause = false;
			Log.d(LOG_TAG, "Requests resumed, notifying");
			synchronized (this) {
				notify();
			}
		}
		
		@Override
		public void run() {
			Log.d(LOG_TAG, "Consumer thread started.");
			Request request = null;
			while (true) {
				try {
					if (mStop) break;  // May lose current request.
					if (mPause) {
						Log.d(LOG_TAG, "Requests paused, waiting.");
						synchronized (this) {
							wait();
						}
						continue;
					}
					
					if (request == null) {
						request = mRequestQueue.takeRequest();  // Blocks waiting for request.
						if (request == null) {
							Log.d(LOG_TAG, "Request not found (maybe stop was requested?)");
							continue;
						}
					}
					String path;
					try {
						path = processRequest(request);
						mCache.put(request.getUrl(), path);
					} catch (MalformedURLException e) {
						Log.w(LOG_TAG, "MalformedURLException", e);
						path = "";  // Returns an invalid path signaling error.
					}
					try {
						// Callback with result.
						if (request.getCallback() != null) {
							request.getCallback().requestFinished(request.getId(), path);
						}
					} catch (RemoteException e) {  // Exception calling remote interface.
						Log.w(LOG_TAG, "RemoteException", e);
					}
					request = null;
				} catch (InterruptedException e) { // Interrupted while waiting to retrieve element.
					Log.w(LOG_TAG, "InterruptedException", e);
					// Will retry same request (request != null).
				} catch (IOException e) {
					Log.w(LOG_TAG, "IOException", e);
					// Will retry same request (request != null).
				}
			}
			Log.d(LOG_TAG, "Consumer thread stopped.");
		}
	}
	
}

	
class Request {
	public static final String METHOD_GET = "GET";
	public static final String METHOD_POST = "POST";
	
//	private static int current_id = 1;  // First ID.
	
//	private int getNextId() {
//		return current_id++;
//	}
	
	private String id;
	private String requestor;
	private IConnectionServiceCallback callback;
	
	private String url;
	private String method;
	private Bundle stringParts;
	private Bundle fileParts;
	
	public Request(String url, IConnectionServiceCallback callback,
			String requestor, String id) {
		this.id = id;
		this.url = url;
		if (!url.startsWith("http://")){
			this.url = "http://" + this.url;
		}
		this.callback = callback;
		this.requestor = requestor;
		this.method = METHOD_GET;
	}
	
	public Request(String url, IConnectionServiceCallback callback,
			String requestor, String id, Bundle stringParts, Bundle fileParts) {
		this(url, callback, requestor, id);
		if (stringParts != null) this.stringParts = stringParts;
		else this.stringParts = new Bundle();
		if (fileParts != null) this.fileParts = fileParts;
		else this.fileParts = new Bundle();
		this.method = METHOD_POST;
	}
	
	public String getId() {
		return id;
	}

	public String getRequestor() {
		return requestor;
	}
	
	public IConnectionServiceCallback getCallback() {
		return callback;
	}

	public String getUrl() {
		return url;
	}
	
//	public String getMethod() {
//		return method;
//	}
//	
//	public Bundle getStringParts() {
//		return stringParts;
//	}
//	
//	public Bundle getFileParts() {
//		return fileParts;
//	}
	
	@Override
	public boolean equals(Object o) {
		if (o instanceof Request) {
			return ((Request) o).getId().equals(this.getId())
				&& ((Request) o).getRequestor().equals(this.getRequestor());
		} else {
			return super.equals(o);
		}
	}

	public HttpResponse execute(HttpClient httpClient, Context context)
			throws ClientProtocolException, IOException {

		if (method.equals(METHOD_GET)) {
			return httpClient.execute(this.getHttpGet());
		} else if (method.equals(METHOD_POST)) {
			return httpClient.execute(this.getHttpPost(context));
		} else throw new IOException("Invalid request method (neither GET nor POST).");
	}
	
	private HttpGet getHttpGet() {
		return new HttpGet(url);
	}
	
	private HttpPost getHttpPost(Context context) {
		HttpPost httpPost = new HttpPost(url);
		MultipartEntity entity = new MultipartEntity();
		
		for (String key : stringParts.keySet()) {
			try {
				entity.addPart(key, new StringBody(stringParts.getString(key)));
			} catch (UnsupportedEncodingException e) {
				Log.w("ConnectionService", "Exception while adding part to post entity.", e);
			}
		}
		
		for (String key : fileParts.keySet()) {
			// TODO quero fazer assim mas não funciona
//			try {
//				entity.addPart(key, new FileBody(new File(new URI(fileParts.getString(key)))));
//			} catch (URISyntaxException e) {
//				Log.w(LOG_TAG, "URI Exception", e);
//			}
			try {
				InputStream is = context.getContentResolver().openInputStream(Uri.parse(fileParts.getString(key)));
					String ss[] = (fileParts.getString(key)).split("/");
				entity.addPart(key, new InputStreamBody(is, ss[ss.length - 1]));
			} catch (FileNotFoundException e) {
				Log.w("ConnectionService#Request", "Exception while adding file to post entity.", e);
			}
		}
		
		httpPost.setEntity(entity);
		return httpPost;
	}
}


class RequestQueue {
	private BlockingQueue<RequestQueueId> mQueue = new LinkedBlockingQueue<RequestQueueId>();
	private Map<RequestQueueId, Request> mRequests = new HashMap<RequestQueueId, Request>();  // Talvez seja possível refatorar de modo que o mapa seja desnecessário.
	
	RequestQueue() { }
	
	public boolean addRequest(Request request) {
		RequestQueueId id = new RequestQueueId(request);
		// É importante colocar no mapa antes, pois a inserção na fila
		// desbloqueia a thread consumidora.
		mRequests.put(id, request);
		if (!mQueue.offer(id)) {
			// Queue is full?
//			Log.w(LOG_TAG, "Queue is full; element not inserted.");
			mRequests.remove(id);
			return false;
		}
		return true;
	}
	
	/**
	 * Retrieves and removes the next request in the queue, waiting if necessary
	 * until a request becomes available.
	 * @return the next request in the queue, or null if the request retrieved was invalid
	 * @throws InterruptedException if interrupted while waiting
	 */
	public Request takeRequest() throws InterruptedException {
		// Retrieves and removes the head of this queue,
		// waiting if no elements are present on this queue.
		RequestQueueId id = mQueue.take();
		Request request = mRequests.get(id);
		if (request != null) mRequests.remove(id);
		return request;
	}
	
	public void removeRequest(String requestor, String id) {
		// É importante remover da fila primeiro para evitar que uma requisição
		// removida seja processada.
		mQueue.remove(id);
		mRequests.remove(id);
	}
	
	public void removeRequests(String requestor) {
		List<Request> matches = new ArrayList<Request>();
		for (Request d : mRequests.values()) {
			if (d.getRequestor().equals(requestor)) {
				matches.add(d);
			}
		}
		for (Request d : matches) {
			RequestQueueId id = new RequestQueueId(d);
			mQueue.remove(id);
			mRequests.remove(id);
		}
		// Tá estranho, mas acho que preciso fazer assim porque o iterador
		// só suporta acesso aos elementos.	
	}
	
	/**
	 * Coloca um elemento só para desbloquear a fila.
	 */
	public void shake() {
		mQueue.offer(new RequestQueueId(null, null));
	}
	
	/**
	 * Identificador para requisições utilizado apenas na fila. Evita conflito
	 * entre requests com mesmo ID utilizando o requestor.
	 */
	public class RequestQueueId {
		private final String requestor;
		private final String id;

		public RequestQueueId(String requestor, String id) {
			this.requestor = requestor;
			this.id = id;
		}
		
		public RequestQueueId(Request request) {
			this(request.getRequestor(), request.getId());
		}

		public int hashCode() {
			int hashFirst = requestor != null ? requestor.hashCode() : 0;
			int hashSecond = id != null ? id.hashCode() : 0;
			
			return (hashFirst + hashSecond) * hashSecond + hashFirst;
		}

		public boolean equals(Object o) {
			if (o instanceof RequestQueueId) {
				RequestQueueId requestQueueId = (RequestQueueId) o;
				return this.requestor.equals(requestQueueId.getRequestor())
						&& this.id.equals(requestQueueId.getId());
			}
			return false;
		}

		public String getRequestor() {
			return requestor;
		}

		public String getId() {
			return id;
		}
	}
}


/**
 * Wrapper around Map.
 */
class Cache {
	Map<String, String> mMap = new HashMap<String, String>();  // URL, file path
	
	/**
	 * @return the path to the file, or null if this url was not in cache
	 */
	public String get(String url) {
		String path = mMap.get(url);
		if (path != null && new File(path).exists()) {
			return path;
		} else {
			mMap.remove(url);
			return null;
		}
	}
	
	public void put(String url, String path) {
		mMap.put(url, path);
	}
}
